from __future__ import annotations

import threading
import time
from typing import Callable, Optional

try:
    import paramiko  # type: ignore
except Exception as exc:  # pragma: no cover - handled at runtime
    paramiko = None  # type: ignore


class SSHClient:
    """
    Minimal SSH client using paramiko with an interactive shell channel.

    on_data: callback receiving decoded str; called from reader thread.
    """

    def __init__(self, encoding: str = "utf-8", on_data: Optional[Callable[[str], None]] = None) -> None:
        self._encoding = encoding
        self._on_data = on_data
        self._client = None  # type: ignore
        self._chan = None  # type: ignore
        self._running = False
        self._t: Optional[threading.Thread] = None

    @property
    def is_connected(self) -> bool:
        return bool(self._chan and not self._chan.closed)

    def connect(self, host: str, port: int, username: str, password: str, timeout: float = 10.0) -> None:
        if paramiko is None:
            raise RuntimeError("paramiko not installed. Please install 'paramiko'.")
        self.disconnect()
        client = paramiko.SSHClient()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        client.connect(hostname=host, port=port, username=username, password=password, timeout=timeout, look_for_keys=False, allow_agent=False)
        chan = client.invoke_shell(term='xterm')
        chan.settimeout(0.0)  # non-blocking
        self._client = client
        self._chan = chan
        self._running = True
        self._t = threading.Thread(target=self._reader, name="SSHReader", daemon=True)
        self._t.start()

    def disconnect(self) -> None:
        self._running = False
        if self._t and self._t.is_alive():
            self._t.join(timeout=1.0)
        self._t = None
        if self._chan:
            try:
                self._chan.close()
            except Exception:
                pass
            self._chan = None
        if self._client:
            try:
                self._client.close()
            except Exception:
                pass
            self._client = None

    def send_raw(self, data: bytes) -> None:
        if not self._chan or self._chan.closed:
            raise RuntimeError("SSH not connected")
        self._chan.send(data)

    def send_line(self, text: str, line_end: str = "\n") -> None:
        payload = (text + line_end).encode(self._encoding, errors="replace")
        self.send_raw(payload)

    # Internal
    def _reader(self) -> None:
        assert self._chan is not None
        while self._running and self._chan and not self._chan.closed:
            try:
                if self._chan.recv_ready():
                    data = self._chan.recv(4096)
                    if data:
                        s = data.decode(self._encoding, errors="replace")
                        if self._on_data:
                            self._on_data(s)
                    else:
                        time.sleep(0.02)
                else:
                    time.sleep(0.02)
            except Exception:
                break
        self._running = False
